﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CodeDom;
using System.Collections;
using System.Globalization;
using System.Reflection;

namespace System.ComponentModel.Design.Serialization;

/// <summary>
///  This serializer serializes collections. This can either create statements or expressions.
///  It will create an expression and assign it to the statement in the current context stack if the object is an array.
///  If it is a collection with an add range or similar method, it will create a statement calling the method.
/// </summary>
public class CollectionCodeDomSerializer : CodeDomSerializer
{
    private static CollectionCodeDomSerializer? s_defaultSerializer;

    /// <summary>
    ///  Retrieves a default static instance of this serializer.
    /// </summary>
    internal static new CollectionCodeDomSerializer Default => s_defaultSerializer ??= new CollectionCodeDomSerializer();

    /// <summary>
    ///  Computes the delta between an existing collection and a modified one.
    ///  This is for the case of inherited items that have collection properties so we only
    ///  generate Add/AddRange calls for the items that have been added.
    ///  It works by Hashing up the items in the original collection and then walking the modified collection
    ///  and only returning those items which do not exist in the base collection.
    /// </summary>
    [return: NotNullIfNotNull(nameof(modified))]
    private static ICollection? GetCollectionDelta(ICollection? original, ICollection? modified)
    {
        if (original is null || modified is null || original.Count == 0)
        {
            return modified;
        }

        IEnumerator modifiedEnum = modified.GetEnumerator();
        if (modifiedEnum is null)
        {
            Debug.Fail($"Collection of type {modified.GetType().FullName} doesn't return an enumerator");
            return modified;
        }

        // first hash up the values so we can quickly decide if it's a new one or not
        Dictionary<object, int> originalValues = [];
        foreach (object originalValue in original)
        {
            // the array could contain multiple copies of the same value (think of a string collection), so we need to be sensitive of that.
            if (originalValues.TryGetValue(originalValue, out int count))
            {
                originalValues[originalValue] = count + 1;
            }
            else
            {
                originalValues.Add(originalValue, 1);
            }
        }

        // now walk through and delete existing values
        List<object>? result = null;
        // now compute the delta.
        for (int i = 0; i < modified.Count && modifiedEnum.MoveNext(); i++)
        {
            object value = modifiedEnum.Current!;

            if (originalValues.TryGetValue(value, out int count))
            {
                // we've got one we need to remove, so create our array list, and push all the values we've passed into it.
                if (result is null)
                {
                    result = [];
                    modifiedEnum.Reset();
                    for (int n = 0; n < i && modifiedEnum.MoveNext(); n++)
                    {
                        result.Add(modifiedEnum.Current!);
                    }

                    // and finally skip the one we're on
                    modifiedEnum.MoveNext();
                }

                // decrement the count if we've got more than one...
                if (--count == 0)
                {
                    originalValues.Remove(value);
                }
                else
                {
                    originalValues[value] = count;
                }
            }
            else // this one isn't in the old list, so add it to our result list.
            {
                result?.Add(value);
            }

            // this item isn't in the list and we haven't yet created our array list so just keep on going.
        }

        return result ?? modified;
    }

    /// <summary>
    ///  Checks the attributes on this method to see if they support serialization.
    /// </summary>
    protected bool MethodSupportsSerialization(MethodInfo method)
    {
        ArgumentNullException.ThrowIfNull(method);

        object[] attributes = method.GetCustomAttributes(typeof(DesignerSerializationVisibilityAttribute), true);
        if (attributes.Length > 0)
        {
            DesignerSerializationVisibilityAttribute visibility = (DesignerSerializationVisibilityAttribute)attributes[0];
            if (visibility is { Visibility: DesignerSerializationVisibility.Hidden })
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    ///  Serializes the given object into a CodeDom object.
    /// </summary>
    public override object? Serialize(IDesignerSerializationManager manager, object value)
    {
        ArgumentNullException.ThrowIfNull(manager);
        ArgumentNullException.ThrowIfNull(value);

        object? result = null;

        // We serialize collections as follows:
        // If the collection is an array, we write out the array.
        // If the collection has a method called AddRange, we will call that, providing an array.
        // If the collection has an Add method, we will call it repeatedly.
        // If the collection is an IList, we will cast to IList and add to it.
        // If the collection has no add method, but is marked with PersistContents,
        // we will enumerate the collection and serialize each element.
        // Check to see if there is a CodePropertyReferenceExpression on the stack.
        // If there is, we can use it as a guide for serialization.
        CodeExpression? target;
        if (manager.TryGetContext(out ExpressionContext? context) && context.PresetValue == value &&
            manager.TryGetContext(out PropertyDescriptor? property) && property.PropertyType == context.ExpressionType)
        {
            // We only want to give out an expression target if this is our context
            // (we find this out by comparing types above) and if the context type is not an array. If it is an array,
            // we will just return the array create expression.
            target = context.Expression;
        }
        else
        {
            // This context is either the wrong context or doesn't match the property descriptor we found.
            target = null;
            context = null;
            property = null;
        }

        // If we have a target expression see if we can create a delta for the collection. We want to do this only if the
        // property the collection is associated with is inherited, and if the collection is not an array.
        if (value is ICollection collection)
        {
            ICollection subset = collection;
            Type collectionType = context?.ExpressionType ?? collection.GetType();
            bool isArray = typeof(Array).IsAssignableFrom(collectionType);

            // If we don't have a target expression and this isn't an array, let's try to create one.
            if (target is null && !isArray)
            {
                target = SerializeCreationExpression(manager, collection, out bool isComplete);
                if (isComplete)
                {
                    return target;
                }
            }

            if (target is not null || isArray)
            {
                if (property is InheritedPropertyDescriptor inheritedDesc && !isArray)
                {
                    subset = GetCollectionDelta(inheritedDesc.OriginalValue as ICollection, collection);
                }

                result = SerializeCollection(manager, target, collectionType, collection, subset);

                // See if we should emit a clear for this collection.
                if (target is not null && ShouldClearCollection(manager, collection))
                {
                    CodeStatementCollection? resultCollection = result as CodeStatementCollection;

                    // If non empty collection is being serialized, but no statements were generated, there is no need to clear.
                    if (collection.Count > 0 && (result is null || (resultCollection is not null && resultCollection.Count == 0)))
                    {
                        return null;
                    }

                    if (resultCollection is null)
                    {
                        resultCollection = [];
                        if (result is CodeStatement resultStatement)
                        {
                            resultCollection.Add(resultStatement);
                        }

                        result = resultCollection;
                    }

                    CodeMethodInvokeExpression clearMethod = new(target, "Clear");
                    CodeExpressionStatement clearStatement = new(clearMethod);
                    resultCollection.Insert(0, clearStatement);
                }
            }
        }
        else
        {
            Debug.Fail($"Collection serializer invoked for non-collection: {(value is null ? "(null)" : value.GetType().Name)}");
        }

        return result;
    }

    /// <summary>
    ///  Given a set of methods and objects, determines the method with the correct of parameter type for all objects.
    /// </summary>
    private static MethodInfo? ChooseMethodByType(TypeDescriptionProvider provider, List<MethodInfo> methods, ICollection values)
    {
        // Note that this method uses reflection types which may not be compatible with runtime types. objType must be
        // obtained from the same provider as the methods were to ensure that the reflection types all belong to the
        // same type universe.
        MethodInfo? final = null;
        Type? finalType = null;
        foreach (object obj in values)
        {
            Type objType = provider.GetReflectionType(obj);
            MethodInfo? candidate = null;
            Type? candidateType = null;
            if (final is null || (finalType is not null && !finalType.IsAssignableFrom(objType)))
            {
                foreach (MethodInfo method in methods)
                {
                    ParameterInfo parameter = method.GetParameters()[0];
                    if (parameter is not null)
                    {
                        Type type = parameter.ParameterType;
                        if (type.IsArray)
                        {
                            type = type.GetElementType()!;
                        }

                        if (type.IsAssignableFrom(objType))
                        {
                            if (final is not null)
                            {
                                if (type.IsAssignableFrom(finalType))
                                {
                                    final = method;
                                    finalType = type;
                                    break;
                                }
                            }
                            else if (candidate is null)
                            {
                                candidate = method;
                                candidateType = type;
                            }
                            else
                            {
                                // we found another method. Pick the one that uses the most derived type.
                                Debug.Assert(candidateType!.IsAssignableFrom(type) || type.IsAssignableFrom(candidateType), "These two types are not related, how were they chosen based on the base type");
                                bool assignable = candidateType.IsAssignableFrom(type);
                                candidate = assignable ? method : candidate;
                                candidateType = assignable ? type : candidateType;
                            }
                        }
                    }
                }
            }

            if (final is null)
            {
                final = candidate;
                finalType = candidateType;
            }
        }

        return final;
    }

    /// <summary>
    ///  Serializes the given collection. targetExpression will refer to the expression used
    ///  to refer to the collection, but it can be null.
    /// </summary>
    protected virtual object? SerializeCollection(IDesignerSerializationManager manager, CodeExpression? targetExpression, Type targetType, ICollection originalCollection, ICollection valuesToSerialize)
    {
        ArgumentNullException.ThrowIfNull(manager);
        ArgumentNullException.ThrowIfNull(targetType);
        ArgumentNullException.ThrowIfNull(originalCollection);
        ArgumentNullException.ThrowIfNull(valuesToSerialize);

        object? result = null;
        bool serialized = false;
        if (typeof(Array).IsAssignableFrom(targetType))
        {
            CodeArrayCreateExpression? arrayCreate = SerializeArray(manager, targetType, (Array)originalCollection, valuesToSerialize);
            if (arrayCreate is not null)
            {
                if (targetExpression is not null)
                {
                    result = new CodeAssignStatement(targetExpression, arrayCreate);
                }
                else
                {
                    result = arrayCreate;
                }
            }
        }
        else if (valuesToSerialize.Count > 0)
        {
            // Use the TargetFrameworkProviderService to create a provider, or use the default for the collection if the
            // service is not available. Since TargetFrameworkProvider reflection types are not compatible with RuntimeTypes,
            // they can only be used with other reflection types from the same provider.
            TypeDescriptionProvider? provider = GetTargetFrameworkProvider(manager, originalCollection);
            provider ??= TypeDescriptor.GetProvider(originalCollection);

            MethodInfo[] methods = provider.GetReflectionType(originalCollection).GetMethods(BindingFlags.Public | BindingFlags.Instance);
            List<MethodInfo> addRangeMethods = [];
            List<MethodInfo> addMethods = [];
            foreach (MethodInfo method in methods)
            {
                switch (method.Name)
                {
                    case "AddRange":
                        ParameterInfo[] parameters = method.GetParameters();
                        if (parameters is [{ ParameterType.IsArray: true }] && MethodSupportsSerialization(method))
                        {
                            addRangeMethods.Add(method);
                        }

                        break;

                    case "Add":
                        if (method.GetParameters().Length == 1 && MethodSupportsSerialization(method))
                        {
                            addMethods.Add(method);
                        }

                        break;
                }
            }

            MethodInfo? addRangeMethodToUse = ChooseMethodByType(provider, addRangeMethods, valuesToSerialize);
            if (addRangeMethodToUse is not null)
            {
                Type elementType = provider.GetRuntimeType(addRangeMethodToUse.GetParameters()[0].ParameterType.GetElementType()!);
                result = SerializeViaAddRange(manager, targetExpression, elementType, valuesToSerialize);
                serialized = true;
            }
            else
            {
                MethodInfo? addMethodToUse = ChooseMethodByType(provider, addMethods, valuesToSerialize);
                if (addMethodToUse is not null)
                {
                    Type elementType = provider.GetRuntimeType(addMethodToUse.GetParameters()[0].ParameterType);
                    result = SerializeViaAdd(manager, targetExpression, elementType, valuesToSerialize);
                    serialized = true;
                }
            }

#pragma warning disable SYSLIB0050 // Type or member is obsolete
            if (!serialized && originalCollection.GetType().IsSerializable)
            {
                result = SerializeToResourceExpression(manager, originalCollection, false);
            }
#pragma warning restore SYSLIB0050
        }

        return result;
    }

    /// <summary>
    ///  Serializes the given array.
    /// </summary>
    private CodeArrayCreateExpression? SerializeArray(IDesignerSerializationManager manager, Type targetType, Array array, ICollection valuesToSerialize)
    {
        CodeArrayCreateExpression? result = null;

        if (array.Rank != 1)
        {
            manager.ReportError(string.Format(SR.SerializerInvalidArrayRank, array.Rank.ToString(CultureInfo.InvariantCulture)));
        }
        else
        {
            // For an array, we need an array create expression. First, get the array type
            Type elementType = targetType.GetElementType()!;
            CodeTypeReference elementTypeRef = new(elementType);

            // Now create an ArrayCreateExpression, and fill its initializers.
            CodeArrayCreateExpression arrayCreate = new CodeArrayCreateExpression
            {
                CreateType = elementTypeRef
            };

            bool arrayOk = true;
            foreach (object o in valuesToSerialize)
            {
                // If this object is being privately inherited, it cannot be inside this collection.
                // Since we're writing an entire array here, we cannot write any of it.
                if (o is IComponent && TypeDescriptor.GetAttributes(o).Contains(InheritanceAttribute.InheritedReadOnly))
                {
                    arrayOk = false;
                    break;
                }

                CodeExpression? expression = null;
                // If there is an expression context on the stack at this point, we need to fix up the ExpressionType
                // on it to be the array element type.
                ExpressionContext? newContext = null;
                if (manager.TryGetContext(out ExpressionContext? context))
                {
                    newContext = new ExpressionContext(context.Expression, elementType, context.Owner);
                    manager.Context.Push(newContext);
                }

                try
                {
                    expression = SerializeToExpression(manager, o);
                }
                finally
                {
                    if (newContext is not null)
                    {
                        Debug.Assert(manager.Context.Current == newContext, "Context stack corrupted.");
                        manager.Context.Pop();
                    }
                }

                if (expression is not null)
                {
                    if (o is not null && o.GetType() != elementType)
                    {
                        expression = new CodeCastExpression(elementType, expression);
                    }

                    arrayCreate.Initializers.Add(expression);
                }
                else
                {
                    arrayOk = false;
                    break;
                }
            }

            if (arrayOk)
            {
                result = arrayCreate;
            }
        }

        return result;
    }

    /// <summary>
    ///  Serializes the given collection by creating multiple calls to an Add method.
    /// </summary>
    private CodeStatementCollection SerializeViaAdd(
        IDesignerSerializationManager manager,
        CodeExpression? targetExpression,
        Type elementType,
        ICollection valuesToSerialize)
    {
        CodeStatementCollection statements = [];

        // Here we need to invoke Add once for each and every item in the collection. We can re-use the property
        // reference and method reference, but we will need to recreate the invoke statement each time.
        CodeMethodReferenceExpression methodRef = new(targetExpression!, "Add");

        if (valuesToSerialize.Count == 0)
        {
            return statements;
        }

        foreach (object o in valuesToSerialize)
        {
            // If this object is being privately inherited, it cannot be inside this collection.
            bool genCode = o is not IComponent;
            if (!genCode)
            {
                if (TypeDescriptorHelper.TryGetAttribute(o, out InheritanceAttribute? ia))
                {
                    genCode = ia.InheritanceLevel != InheritanceLevel.InheritedReadOnly;
                }
                else
                {
                    genCode = true;
                }
            }

            Debug.Assert(genCode, "Why didn't GetCollectionDelta calculate the same thing?");
            if (genCode)
            {
                CodeMethodInvokeExpression statement = new CodeMethodInvokeExpression
                {
                    Method = methodRef
                };

                CodeExpression? serializedObject = null;

                // If there is an expression context on the stack at this point,
                // we need to fix up the ExpressionType on it to be the element type.
                ExpressionContext? newCtx = null;

                if (manager.TryGetContext(out ExpressionContext? ctx))
                {
                    newCtx = new ExpressionContext(ctx.Expression, elementType, ctx.Owner);
                    manager.Context.Push(newCtx);
                }

                try
                {
                    serializedObject = SerializeToExpression(manager, o);
                }
                finally
                {
                    if (newCtx is not null)
                    {
                        Debug.Assert(manager.Context.Current == newCtx, "Context stack corrupted.");
                        manager.Context.Pop();
                    }
                }

                if (o is not null && !elementType.IsAssignableFrom(o.GetType()) && o.GetType().IsPrimitive)
                {
                    serializedObject = new CodeCastExpression(elementType, serializedObject!);
                }

                if (serializedObject is not null)
                {
                    statement.Parameters.Add(serializedObject);
                    statements.Add(statement);
                }
            }
        }

        return statements;
    }

    /// <summary>
    ///  Serializes the given collection by creating an array and passing it to the AddRange method.
    /// </summary>
    private CodeStatementCollection SerializeViaAddRange(
        IDesignerSerializationManager manager,
        CodeExpression? targetExpression,
        Type elementType,
        ICollection valuesToSerialize)
    {
        CodeStatementCollection statements = [];

        if (valuesToSerialize.Count == 0)
        {
            return statements;
        }

        List<CodeExpression> arrayList = new(valuesToSerialize.Count);
        foreach (object o in valuesToSerialize)
        {
            // If this object is being privately inherited, it cannot be inside this collection.
            bool genCode = o is not IComponent;
            if (!genCode)
            {
                if (TypeDescriptorHelper.TryGetAttribute(o, out InheritanceAttribute? ia))
                {
                    genCode = ia.InheritanceLevel != InheritanceLevel.InheritedReadOnly;
                }
                else
                {
                    genCode = true;
                }
            }

            Debug.Assert(genCode, "Why didn't GetCollectionDelta calculate the same thing?");
            if (genCode)
            {
                CodeExpression? expression = null;
                // If there is an expression context on the stack at this point, we need to fix up the ExpressionType
                // on it to be the element type.
                ExpressionContext? newContext = null;

                if (manager.TryGetContext(out ExpressionContext? ctx))
                {
                    newContext = new ExpressionContext(ctx.Expression, elementType, ctx.Owner);
                    manager.Context.Push(newContext);
                }

                try
                {
                    expression = SerializeToExpression(manager, o);
                }
                finally
                {
                    if (newContext is not null)
                    {
                        Debug.Assert(manager.Context.Current == newContext, "Context stack corrupted.");
                        manager.Context.Pop();
                    }
                }

                if (expression is not null)
                {
                    // Check to see if we need a cast
                    if (o is not null && !elementType.IsAssignableFrom(o.GetType()))
                    {
                        expression = new CodeCastExpression(elementType, expression);
                    }

                    arrayList.Add(expression);
                }
            }
        }

        if (arrayList.Count > 0)
        {
            // Now convert the array list into an array create expression.
            CodeTypeReference elementTypeRef = new(elementType);

            // Now create an ArrayCreateExpression, and fill its initializers.
            CodeArrayCreateExpression arrayCreate = new CodeArrayCreateExpression
            {
                CreateType = elementTypeRef
            };

            foreach (CodeExpression expression in arrayList)
            {
                arrayCreate.Initializers.Add(expression);
            }

            CodeMethodReferenceExpression methodRef = new(targetExpression!, "AddRange");
            CodeMethodInvokeExpression methodInvoke = new CodeMethodInvokeExpression
            {
                Method = methodRef
            };

            methodInvoke.Parameters.Add(arrayCreate);
            statements.Add(new CodeExpressionStatement(methodInvoke));
        }

        return statements;
    }

    /// <summary>
    ///  Returns true if we should clear the collection contents.
    /// </summary>
    private bool ShouldClearCollection(IDesignerSerializationManager manager, ICollection collection)
    {
        bool shouldClear = false;
        PropertyDescriptor? clearProperty = manager.Properties["ClearCollections"];
        if (clearProperty is not null && clearProperty.TryGetValue(manager, out bool b) && b)
        {
            shouldClear = true;
        }

        if (!shouldClear)
        {
            PropertyDescriptor? property = manager.GetContext<PropertyDescriptor>();
            if (manager.TryGetContext(out SerializeAbsoluteContext? absolute) && absolute.ShouldSerialize(property))
            {
                shouldClear = true;
            }
        }

        if (shouldClear)
        {
            MethodInfo? clearMethod = TypeDescriptor.GetReflectionType(collection).GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance, null, [], null);
            if (clearMethod is null || !MethodSupportsSerialization(clearMethod))
            {
                shouldClear = false;
            }
        }

        return shouldClear;
    }
}
